package org.cocos2d.utils;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

/*
 * Plist parser.
 * Supports:
 *  - string     -> String
 *  - integer    -> Integer
 *  - real       -> Double
 *  - date       -> Date
 *  - true/false -> Boolean
 *  - data       -> byte[]
 *  - dict       -> HashMap<String, Object>
 *  - array      -> ArrayList<Object>
 */

public class PlistParser extends DefaultHandler {

    // type is used in characters()
    private static final int TYPE_UNKNOWN = 0;
    private static final int TYPE_KEY = 1;
    private static final int TYPE_STRING = 2;
    private static final int TYPE_INTEGER = 3;
    private static final int TYPE_DATA = 4;
    private static final int TYPE_DATE = 5;
    private static final int TYPE_REAL = 6;
    // for tag name
    private static final String EL_KEY = "key";
    private static final String EL_STRING = "string";
    private static final String EL_INTEGER = "integer";
    private static final String EL_DATA = "data";
    private static final String EL_DATE = "date";
    private static final String EL_REAL = "real";
    private static final String EL_TRUE = "true";
    private static final String EL_FALSE = "false";
    private static final String EL_DICT = "dict";
    private static final String EL_ARRAY = "array";
    private static final int COLL_UNKNOWN = 0;
    private static final int COLL_DICT = 1;
    private static final int COLL_ARRAY = 2;
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'");
    // root hashmap which should be returned finally
    private Object rootDict;
    private String currentKey;
    private int currentElement = TYPE_UNKNOWN;
    private int currentCollectionType = COLL_UNKNOWN;
    private Object currentCollection;
    private ArrayList<Object> depthStack = new ArrayList<Object>();
    private StringBuilder characterData = new StringBuilder();

    private PlistParser() {
    }

    public static HashMap<String, Object> parse(String filename) {
        try {
            InputStream in = ContentHelper.sharedHelper().openInputStream(filename);
            return parse(in);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

    public static HashMap<String, Object> parse(InputStream in) {
        @SuppressWarnings("unchecked")
        HashMap<String, Object> ret = (HashMap<String, Object>) parsePlist(in);
        return ret;
    }

    public static ArrayList<Object> parseArray(String filename) {
        try {
            InputStream in = ContentHelper.sharedHelper().openInputStream(filename);
            return parseArray(in);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

    public static ArrayList<Object> parseArray(InputStream in) {
        @SuppressWarnings("unchecked")
        ArrayList<Object> ret = (ArrayList<Object>) parsePlist(in);
        return ret;
    }

    private static Object parsePlist(InputStream in) {
        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser parser = factory.newSAXParser();
            PlistParser handler = new PlistParser();

            BufferedReader reader =
                    new BufferedReader(new InputStreamReader(in), 8192);


            parser.parse(new InputSource(reader), handler);

            return handler.rootDict;

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void setupCurrentCollection(Object coll) {
        currentCollection = coll;
        if (coll == null) {
            currentCollectionType = COLL_UNKNOWN;
        } else if (coll instanceof HashMap<?, ?>) {
            currentCollectionType = COLL_DICT;
        } else { // array
            currentCollectionType = COLL_ARRAY;
        }
    }

    private void depthUp(Object newcoll) {
        addToCollection(newcoll);
        depthStack.add(newcoll);
        setupCurrentCollection(newcoll);
    }

    private void depthDown() {
        int s = depthStack.size();
        if (s > 0) {
            depthStack.remove(s - 1);

            Object currCol = null;
            if (s > 1) {
                currCol = depthStack.get(s - 2);
            }
            setupCurrentCollection(currCol);
        }
    }

    @SuppressWarnings("unchecked")
    private void addToCollection(Object obj) {
        switch (currentCollectionType) {
            case COLL_DICT:
                ((HashMap<String, Object>) currentCollection).put(currentKey, obj);
                break;
            case COLL_ARRAY:
                ((ArrayList<Object>) currentCollection).add(obj);
                break;
            case COLL_UNKNOWN: // first call could be array or dict
                rootDict = obj;
                break;
        }
    }

    @Override
    public void startElement(String uri, String localName, String qName,
                             Attributes attributes) throws SAXException {
        if (localName.equals(EL_KEY)) {
            currentElement = TYPE_KEY;
        } else if (localName.equals(EL_STRING)) {
            currentElement = TYPE_STRING;
        } else if (localName.equals(EL_INTEGER)) {
            currentElement = TYPE_INTEGER;
        } else if (localName.equals(EL_REAL)) {
            currentElement = TYPE_REAL;
        } else if (localName.equals(EL_DATA)) {
            currentElement = TYPE_DATA;
        } else if (localName.equals(EL_DATE)) {
            currentElement = TYPE_DATE;
        } else if (localName.equals(EL_TRUE)) {
            addToCollection(true);
        } else if (localName.equals(EL_FALSE)) {
            addToCollection(false);

            // collections
        } else if (localName.equals(EL_DICT)) {
            depthUp(new HashMap<String, Object>());
        } else if (localName.equals(EL_ARRAY)) {
            depthUp(new ArrayList<Object>());
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName)
            throws SAXException {

        switch (currentElement) {
            case TYPE_KEY:
                currentKey = characterData.toString().trim();
                break;
            case TYPE_STRING:
                addToCollection(characterData.toString().trim());
                break;
            case TYPE_DATA:
                try {
                    addToCollection(Base64.decode(characterData.toString().trim()));
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                break;
            case TYPE_DATE:
                try {
                    addToCollection(dateFormat.parse(characterData.toString().trim()));
                } catch (ParseException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                break;
            case TYPE_INTEGER:
                addToCollection(new Integer(characterData.toString().trim()));
                break;
            case TYPE_REAL:
                addToCollection(new Double(characterData.toString().trim()));
                break;
            default:
                break;
        }

        currentElement = TYPE_UNKNOWN;
        characterData.setLength(0);

        if (localName.equals(EL_DICT) || localName.equals(EL_ARRAY)) {
            depthDown();
        }
    }

    @Override
    public void characters(char[] _chars, int _start, int _len) {

        characterData.append(_chars, _start, _len);
    }

}
